 /*******************************************************************************
  * Copyright (c) 2006, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/

 package org.eclipse.ui.ide.undo;

 import java.util.HashMap ;
 import java.util.Iterator ;
 import java.util.Map ;

 import org.eclipse.core.resources.IMarker;
 import org.eclipse.core.resources.IResource;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.core.runtime.jobs.ISchedulingRule;
 import org.eclipse.core.runtime.jobs.MultiRule;
 import org.eclipse.ui.internal.ide.undo.MarkerDescription;
 import org.eclipse.ui.internal.ide.undo.UndoMessages;

 /**
  * An AbstractMarkersOperation represents an undoable operation that affects
  * markers on a resource. It provides implementations for marker creation,
  * deletion, and updating. Clients may call the public API from a background
  * thread.
  *
  * This class is not intended to be subclassed by clients.
  *
  * @since 3.3
  *
  */
 abstract class AbstractMarkersOperation extends AbstractWorkspaceOperation {

     MarkerDescription[] markerDescriptions;

     IMarker[] markers;

     Map [] attributes;

     /**
      * Create an AbstractMarkersOperation by specifying a combination of markers
      * and attributes or marker descriptions.
      *
      * @param markers
      * the markers used in the operation or <code>null</code> if no
      * markers yet exist
      * @param markerDescriptions
      * the marker descriptions that should be used to create markers,
      * or <code>null</code> if the markers already exist
      * @param attributes
      * The map of attributes that should be assigned to any existing
      * markers when the markers are updated. Ignored if the markers
      * parameter is <code>null</code>.
      * @param name
      * the name used to describe the operation
      */
     AbstractMarkersOperation(IMarker[] markers,
             MarkerDescription[] markerDescriptions, Map attributes, String name) {
         super(name);
         this.markers = markers;
         this.attributes = null;
         // If there is more than one marker, create an array with a copy
 // of the attributes map for each marker. Keeping a unique map
 // per marker allows us to support the scenario where attributes
 // are merged when updated. In this case, each marker's attributes
 // may differ since their original attributes may have differed.
 if (attributes != null && markers != null) {
             if (markers.length > 1) {
                 this.attributes = new Map [markers.length];
                 for (int i = 0; i < markers.length; i++) {
                     Map copiedAttributes = new HashMap ();
                     copiedAttributes.putAll(attributes);
                     this.attributes[i] = copiedAttributes;
                 }
             } else {
                 this.attributes = new Map [] { attributes };
             }
         }
         setMarkerDescriptions(markerDescriptions);
     }

     /**
      * Delete any currently known markers and save their information in marker
      * descriptions so that they can be restored.
      *
      * @param work
      * the number of work ticks to be used by the delete
      * @param monitor
      * the progress monitor to use for the delete
      * @throws CoreException
      * propagates any CoreExceptions thrown from the resources API
      *
      */
     protected void deleteMarkers(int work, IProgressMonitor monitor)
             throws CoreException {
         if (markers == null || markers.length == 0) {
             monitor.worked(work);
             return;
         }
         int markerWork = work / markers.length;
         markerDescriptions = new MarkerDescription[markers.length];
         for (int i = 0; i < markers.length; i++) {
             markerDescriptions[i] = new MarkerDescription(markers[i]);
             markers[i].delete();
             monitor.worked(markerWork);
         }
         markers = new IMarker[0];
     }

     /**
      * Create markers from any currently known marker descriptions.
      *
      * @param work
      * the number of work ticks to be used by the create
      * @param monitor
      * the progress monitor to use for the create
      * @throws CoreException
      * propagates any CoreExceptions thrown from the resources API
      */
     protected void createMarkers(int work, IProgressMonitor monitor)
             throws CoreException {
         if (markerDescriptions == null || markerDescriptions.length == 0) {
             monitor.worked(work);
             return;
         }
         int markerWork = work / markerDescriptions.length;
         markers = new IMarker[markerDescriptions.length];

         // Recreate the markers from the descriptions
 for (int i = 0; i < markerDescriptions.length; i++) {
             markers[i] = markerDescriptions[i].createMarker();
             monitor.worked(markerWork);
         }
     }

     /**
      * Update the currently known markers with the corresponding array of marker
      * descriptions.
      *
      * @param work
      * the number of work ticks to be used by the update
      * @param monitor
      * the progress monitor to use for the update
      * @param mergeAttributes
      * a boolean specifying whether the attributes are merged or
      * considered to be a replacement of the previous attributes.
      * @throws CoreException
      * propagates any CoreExceptions thrown from the resources API
      *
      */
     protected void updateMarkers(int work, IProgressMonitor monitor,
             boolean mergeAttributes) throws CoreException {
         if (attributes == null || markers == null
                 || attributes.length != markers.length || markers.length == 0) {
             monitor.worked(work);
             return;
         }
         int markerWork = work / markers.length;
         for (int i = 0; i < markers.length; i++) {
             if (mergeAttributes) {
                 Map oldAttributes = markers[i].getAttributes();
                 int increment = markerWork / attributes[i].size();
                 Map replacedAttributes = new HashMap ();

                 for (Iterator iter = attributes[i].keySet().iterator(); iter
                         .hasNext();) {
                     String key = (String ) iter.next();
                     Object val = attributes[i].get(key);
                     markers[i].setAttribute(key, val);
                     replacedAttributes.put(key, oldAttributes.get(key));
                     monitor.worked(increment);
                 }
                 attributes[i] = replacedAttributes;
             } else {
                 // replace all of the attributes
 Map oldAttributes = markers[i].getAttributes();
                 markers[i].setAttributes(attributes[i]);
                 attributes[i] = oldAttributes;
             }
         }
     }

     /**
      * Set the marker descriptions that describe markers that can be created.
      *
      * @param descriptions
      * the descriptions of markers that can be created.
      */
     protected void setMarkerDescriptions(MarkerDescription[] descriptions) {
         markerDescriptions = descriptions;
         addUndoContexts();
         updateTargetResources();
     }

     /*
      * Update the target resources by traversing the currently known markers or
      * marker descriptions and getting their resources.
      */

     private void updateTargetResources() {
         IResource[] resources = null;
         if (markers == null) {
             if (markerDescriptions != null) {
                 resources = new IResource[markerDescriptions.length];
                 for (int i = 0; i < markerDescriptions.length; i++) {
                     resources[i] = markerDescriptions[i].getResource();
                 }
             }
         } else {
             resources = new IResource[markers.length];
             for (int i = 0; i < markers.length; i++) {
                 resources[i] = markers[i].getResource();
             }
         }
         setTargetResources(resources);
     }

     /*
      * Add undo contexts according to marker types. Any unknown marker types
      * will cause the workspace undo context to be added.
      *
      * This is an optimization that allows us to add specific undo contexts for
      * tasks and bookmarks, without also adding the workspace undo context. Note
      * that clients with different marker types may still assign their own
      * specific undo context using AbstractOperation.addContext(IUndoContext) in
      * addition to the workspace context assigned by this method.
      */

     private void addUndoContexts() {
         String [] types = null;
         if (markers == null) {
             if (markerDescriptions != null) {
                 types = new String [markerDescriptions.length];
                 for (int i = 0; i < markerDescriptions.length; i++) {
                     types[i] = markerDescriptions[i].getType();
                 }
             }
         } else {
             types = new String [markers.length];
             for (int i = 0; i < markers.length; i++) {
                 try {
                     types[i] = markers[i].getType();
                 } catch (CoreException e) {
                 }

             }
         }
         if (types != null) {
             for (int i = 0; i < types.length; i++) {
                 // Marker type could be null if marker did not exist.
 // This shouldn't happen, but can.
 // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=158129
 if (types[i] != null) {
                     if (types[i].equals(IMarker.BOOKMARK)) {
                         addContext(WorkspaceUndoUtil.getBookmarksUndoContext());
                     } else if (types[i].equals(IMarker.TASK)) {
                         addContext(WorkspaceUndoUtil.getTasksUndoContext());
                     } else if (types[i] != null) {
                         // type is not known, use the workspace undo context
 addContext(WorkspaceUndoUtil.getWorkspaceUndoContext());
                     }
                 }
             }
         }
     }

     /**
      * Return the array of markers that has been updated or created.
      *
      * @return the array of markers that have been updated or created, or
      * <code>null</code> if no markers have been created or updated.
      */
     public IMarker[] getMarkers() {
         return markers;
     }

     /**
      * Return whether the markers known by this operation currently exist.
      *
      * @return <code>true</code> if there are existing markers and
      * <code>false</code> if there are no known markers or any one of
      * them does not exist
      */
     protected boolean markersExist() {
         if (markers == null || markers.length == 0) {
             return false;
         }
         for (int i = 0; i < markers.length; i++) {
             if (!markers[i].exists()) {
                 return false;
             }
         }
         return true;

     }

     /**
      * Return a status indicating the projected outcome of undoing the marker
      * operation. The receiver is not responsible for remembering the result of
      * this computation.
      *
      * @return the status indicating whether the operation can be undone
      */
     protected abstract IStatus getBasicUndoStatus();

     /**
      * Return a status indicating the projected outcome of redoing the marker
      * operation. The receiver is not responsible for remembering the result of
      * this computation.
      *
      * @return the status indicating whether the operation can be undone
      */
     protected abstract IStatus getBasicRedoStatus();

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.ui.ide.undo.AbstractWorkspaceOperation#computeExecutionStatus(org.eclipse.core.runtime.IProgressMonitor)
      */
     public IStatus computeExecutionStatus(IProgressMonitor monitor) {
         IStatus status = getBasicRedoStatus();
         if (status.isOK()) {
             return super.computeExecutionStatus(monitor);
         }
         if (status.getSeverity() == IStatus.ERROR) {
             markInvalid();
         }
         return status;
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.ui.ide.undo.AbstractWorkspaceOperation#computeUndoableStatus(org.eclipse.core.runtime.IProgressMonitor)
      */
     public IStatus computeUndoableStatus(IProgressMonitor monitor) {
         IStatus status = getBasicUndoStatus();
         if (status.isOK()) {
             return super.computeUndoableStatus(monitor);
         }
         if (status.getSeverity() == IStatus.ERROR) {
             markInvalid();
         }
         return status;
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.ui.ide.undo.AbstractWorkspaceOperation#computeRedoableStatus(org.eclipse.core.runtime.IProgressMonitor)
      */
     public IStatus computeRedoableStatus(IProgressMonitor monitor) {
         IStatus status = getBasicRedoStatus();
         if (status.isOK()) {
             return super.computeRedoableStatus(monitor);
         }
         if (status.getSeverity() == IStatus.ERROR) {
             markInvalid();
         }
         return status;
     }

     /**
      * Compute the status for deleting any known markers. A status severity of
      * <code>OK</code> indicates that the delete is likely to be successful. A
      * status severity of <code>ERROR</code> indicates that the operation is
      * no longer valid. Other status severities are open to interpretation by
      * the caller.
      *
      * @return the status indicating the projected outcome of deleting the
      * markers.
      *
      */
     protected IStatus getMarkerDeletionStatus() {
         if (markersExist()) {
             return Status.OK_STATUS;
         }
         return getErrorStatus(UndoMessages.MarkerOperation_MarkerDoesNotExist);
     }

     /**
      * Compute the status for creating any known markers. A status severity of
      * <code>OK</code> indicates that the create is likely to be successful. A
      * status severity of <code>ERROR</code> indicates that the operation is
      * no longer valid. Other status severities are open to interpretation by
      * the caller.
      *
      * @return the status indicating the projected outcome of creating the
      * markers.
      *
      */
     protected IStatus getMarkerCreationStatus() {
         if (!resourcesExist()) {
             return getErrorStatus(UndoMessages.MarkerOperation_ResourceDoesNotExist);
         } else if (markerDescriptions == null) {
             return getErrorStatus(UndoMessages.MarkerOperation_NotEnoughInfo);
         }
         return Status.OK_STATUS;
     }

     /**
      * Compute the status for updating any known markers. A status severity of
      * <code>OK</code> indicates that the update is likely to be successful. A
      * status severity of <code>ERROR</code> indicates that the operation is
      * no longer valid. Other status severities are open to interpretation by
      * the caller.
      *
      * @return the status indicating the projected outcome of updating the
      * markers.
      *
      */
     protected IStatus getMarkerUpdateStatus() {
         if (!markersExist()) {
             return getErrorStatus(UndoMessages.MarkerOperation_MarkerDoesNotExist);
         } else if (attributes == null) {
             return getErrorStatus(UndoMessages.MarkerOperation_NotEnoughInfo);
         }
         return Status.OK_STATUS;
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.ui.ide.undo.AbstractWorkspaceOperation#getExecuteSchedulingRule()
      */
     protected ISchedulingRule getExecuteSchedulingRule() {
         ISchedulingRule[] ruleArray = new ISchedulingRule[resources.length];
         for (int i = 0; i < resources.length; i++) {
             ruleArray[i] = getWorkspaceRuleFactory().markerRule(resources[i]);
         }
         return MultiRule.combine(ruleArray);
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.ui.ide.undo.AbstractWorkspaceOperation#getUndoSchedulingRule()
      */
     protected ISchedulingRule getUndoSchedulingRule() {
         return getExecuteSchedulingRule();
     }

     /*
      * (non-Javadoc)
      *
      * @see org.eclipse.ui.ide.undo.AbstractWorkspaceOperation#appendDescriptiveText(java.lang.StringBuffer)
      */
     protected void appendDescriptiveText(StringBuffer text) {
         super.appendDescriptiveText(text);
         text.append(" markers: "); //$NON-NLS-1$
 text.append(markers);
         text.append('\'');
         text.append(" markerDescriptions: "); //$NON-NLS-1$
 text.append(markerDescriptions);
         text.append('\'');
         text.append(" attributes: "); //$NON-NLS-1$
 text.append(attributes);
         text.append('\'');
     }
 }

